/**
 * Project: d20Engine
 * Created: Aug 13, 2006 by bebopJMM
 *------------------------------------------------------------------------------
 * $Id$
 */
package org.rollinitiative.d20.entity;

import java.util.ArrayList;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.StringTokenizer;
import java.util.UUID;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.rollinitiative.d20.AdjustableValue;
import org.rollinitiative.d20.Adjustment;
import org.rollinitiative.d20.combat.Attacks;
import org.rollinitiative.d20.combat.Defense;
import org.rollinitiative.d20.entity.HitPoints.LifeState;
import org.rollinitiative.d20.entity.Movement.EncumberanceType;
import org.rollinitiative.d20.entity.skills.Skill;
import org.rollinitiative.d20.entity.skills.SkillsCollection;
import org.rollinitiative.d20.entity.talents.Talent;
import org.rollinitiative.d20.items.Inventory;
import org.rollinitiative.d20.items.Item;

/**
 * An actor is an entity in the game capable of performing independent actions.
 * 
 * @author bebopjmm
 * 
 */
public abstract class Actor implements Comparable
{
    static final Log LOG = LogFactory.getLog(Actor.class);

    public static final String DEFAULT_NAME = "Nameless Actor";

    public enum Modifiers {
        AbilityMod;
    }

    public enum Values {
        Ability, Defense;
    }

    private final UUID uuid;

    private String name;

    private String raceID;

    EnumMap<Ability, AbilityValue> abilities;

    EnumMap<Save, AbilityListenerValue> saves;

    SkillsCollection skills;

    ArrayList<Talent> talents;

    Size currentSize;

    Alignment currentAlign;

    Gender currentGender_;

    AbilityListenerValue initiativeMod;

    Attacks attackStats;

    Defense defenseStats;

    Movement movement;

    EncumberanceType currentEncumberance = EncumberanceType.LIGHT;

    HitPoints hp;

    Inventory inventory;


    /**
     * Instantiates a new actor with DEFAULT_NAME. All abilities are initialized to
     * AbilityValue.DEFAULT_VALUE.
     * 
     * @since incr-0.1
     */
    public Actor()
    {
        this(Actor.DEFAULT_NAME);

    }


    /**
     * Instantiates a new actor with the provided name. All abilities are initialized to
     * AbilityValue.DEFAULT_VALUE.
     * 
     * @param name The full name of this Actor
     * @since incr-0.1
     */
    public Actor(String name)
    {
        this.uuid = UUID.randomUUID();
        this.name = name;
        this.raceID = "NOT-SET";
        this.abilities = new EnumMap<Ability, AbilityValue>(Ability.class);
        for (Ability a : EnumSet.range(Ability.STR, Ability.CHA)) {
            this.abilities.put(a, new AbilityValue(AbilityValue.DEFAULT_VALUE, a));
        }
        // Initialize the saves
        this.saves = new EnumMap<Save, AbilityListenerValue>(Save.class);
        AbilityListenerValue fortSave = new AbilityListenerValue(0, abilities.get(Ability.CON));
        this.saves.put(Save.FORTITUDE, fortSave);
        AbilityListenerValue reflSave = new AbilityListenerValue(0, abilities.get(Ability.DEX));
        this.saves.put(Save.REFLEX, reflSave);
        AbilityListenerValue willSave = new AbilityListenerValue(0, abilities.get(Ability.WIS));
        this.saves.put(Save.WILL, willSave);

        // initialize hit points.
        this.hp = new HitPoints(abilities.get(Ability.CON), true);

        // initialize with no known skills
        // this.skills_ = new Hashtable<Skill, AbilityListenerValue>();
        this.skills = new SkillsCollection();

        // initialize with no known talents
        this.talents = new ArrayList<Talent>();

        currentSize = Size.MEDIUM;

        initiativeMod = new AbilityListenerValue(0, abilities.get(Ability.DEX));
        attackStats = new Attacks(currentSize, abilities.get(Ability.STR), abilities
                .get(Ability.DEX), abilities.get(Ability.STR));
        defenseStats = new Defense(currentSize, abilities.get(Ability.DEX));

        this.movement = new Movement(30, 20, 20);

        this.inventory = new Inventory();
    }


    public UUID getActorID()
    {
        return uuid;
    }


    public String getRaceID()
    {
        return raceID;
    }


    public HitPoints getHitPoints()
    {
        return hp;
    }


    /**
     * This method returns the current hit points for the actor.
     * 
     * @return current hit points for the actor.
     */
    public int getHP()
    {
        return hp.getCurrentHP();
    }


    /**
     * This method returns the maximum hit points for the actor.
     * 
     * @return maximum hit points for the actor.
     */
    public int getMaxHP()
    {
        return hp.getMaxHP();
    }


    /**
     * This method inflicts the designated amount of hit point damage to the actor.
     * 
     * @param hp amount of damage to inflict
     * @return LifeState following the damage.
     */
    public LifeState damage(int hp)
    {
        synchronized (this.hp) {
            return this.hp.damage(hp);
        }
    }


    /**
     * This method cures the designated amount of hit points to the actor.
     * 
     * @param hp amount of curing to apply
     * @return LifeState following the curing
     */
    public LifeState heal(int hp)
    {
        synchronized (this.hp) {
            return this.hp.heal(hp);
        }
    }


    /**
     * The actor's initiative modifier is a value derived from dexterity bonus and other modifiers.
     * 
     * @return the actor's modifier to initiative rolls.
     * @since incr-0.1
     */
    public AbilityListenerValue getInitiativeMod()
    {
        return initiativeMod;
    }


    /**
     * This method revises the baseValue of the designated ability.
     * 
     * @param ability Ability to be modified
     * @param baseValue new baseValue for the designated ability
     */
    public void setAbilityBaseValue(Ability ability, int baseValue)
    {
        LOG.debug("Setting " + ability + " baseValue to " + baseValue);
        AbilityValue abilVal = abilities.get(ability);
        abilVal.setBase(baseValue);
    }


    /**
     * Accessor for the Actor's name.
     * 
     * @return Returns the name.
     * @since incr-0.1
     */
    public String getName()
    {
        return name;
    }


    /**
     * Allows specifying the name of this Actor.
     * 
     * @param name The name to set.
     * @since incr-0.1
     */
    public void setName(String name)
    {
        this.name = name;
    }


    /**
     * Accessor for the Actor's current Size.
     * 
     * @return the actor's currentSize
     * @since incr-0.1
     */
    public Size getCurrentSize()
    {
        return currentSize;
    }


    /**
     * Allows specifying the Size of an Actor
     * 
     * @param currentSize new size for the actor
     * @since incr-0.1
     */
    public void setCurrentSize(Size currentSize)
    {
        this.currentSize = currentSize;
        attackStats.setSize(this.currentSize);
        defenseStats.setSize(this.currentSize);
    }


    /**
     * Accessor for the Actor's Alignment.
     * 
     * @return the actor's current alignment
     * @since incr-0.1
     */
    public Alignment getCurrentAlign()
    {
        return currentAlign;
    }


    /**
     * @param currentAlign new value for current alignment
     */
    public void setCurrentAlign(Alignment currentAlign)
    {
        this.currentAlign = currentAlign;
    }


    /**
     * @return the actor's current gender
     */
    public Gender getCurrentGender()
    {
        return currentGender_;
    }


    /**
     * @param currentGender new value for the current gender
     */
    public void setCurrentGender(Gender currentGender)
    {
        this.currentGender_ = currentGender;
    }


    public AdjustableValue getAbility(Ability ability)
    {
        return abilities.get(ability);
    }


    public AdjustableValue getSave(Save save)
    {
        return saves.get(save);
    }


    public AdjustableValue getSkill(Skill skill)
    {
        AdjustableValue value = skills.getSkill(skill);
        if (value == null) {
            LOG.warn("Requested Skill (" + skill.getId() + ") is not known by this actor: " + name);
        }
        return value;
    }


    public Skill[] getSkills()
    {
        return skills.getSkills();
    }


    /**
     * This method returns true if the actor has the specified skill in their repertoire of known skills.
     * 
     * @param skill Skill to check for
     * @return true if the actor has the specified skill in their repertoire of known skills, otherwise false. 
     */
    public boolean hasSkill(Skill skill)
    {
        return skills.hasSkill(skill);
    }


    public void advanceSkill(Skill skill, int points)
    {
        if (!skills.hasSkill(skill)) {
            LOG.info("Initializing new skill before training: " + skill.getName());
            skills.init(skill, abilities);
        }
        skills.train(skill, points);
    }


    public void initSkill(Skill skill)
    {        
        skills.init(skill, abilities);
    }


    public Talent[] getTalents()
    {
        return talents.toArray(new Talent[talents.size()]);
    }


    public void addTalent(Talent newTalent)
    {
        if (talents.contains(newTalent)) {
            LOG.warn("Ignoring attempt to add talent (" + newTalent.getName()
                    + ") that already exists for actor: " + this.name);
            return;
        }
        else {
            LOG.debug("Adding new talent (" + newTalent.getName() + ") to actor: " + this.name);
            talents.add(newTalent);
        }
    }


    public int getCurrentSpeed()
    {
        return movement.getSpeed(currentEncumberance);
    }


    public Attacks getAttacks()
    {
        return this.attackStats;
    }


    public Defense getDefense()
    {
        return this.defenseStats;
    }


    public void setMaxBaseAttackBonus(int maxBonus)
    {
        this.attackStats.setBaseAttackBonus(maxBonus);
    }
    
    public Item[] getCurrentInventory()
    {
        return inventory.getContents();
    }


    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Comparable#compareTo(java.lang.Object)
     */
    public int compareTo(Object object)
    {
        if (object instanceof Actor) {
            return uuid.compareTo(((Actor) object).uuid);
        }
        return 0;
    }


    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj)
    {
        if (obj instanceof Actor) {
            Actor a = (Actor) obj;
            return a.uuid.equals(this.uuid);
        }
        return super.equals(obj);
    }


    /**
     * This method assigns racial information to the actor.
     * 
     * @param raceID racial identification
     * @param raceSize Size attribute based upon race
     * @param abilAdjustments Racial abilities adjustments to be applied
     */
    void setRace(String raceID, Size raceSize, Hashtable<Ability, Adjustment> abilAdjustments)
    {
        this.raceID = raceID;
        setCurrentSize(raceSize);
        // Apply racial modifiers to abilities
        Enumeration<Ability> abilEnumeration = abilAdjustments.keys();
        while (abilEnumeration.hasMoreElements()) {
            Ability abil = abilEnumeration.nextElement();
            AdjustableValue abilVal = abilities.get(abil);
            Adjustment abilAdj = abilAdjustments.get(abil);
            abilVal.addAdjustment(abilAdj);
        }
    }


    void setBaseMovement(EncumberanceType encumberance, int speed)
    {
        AdjustableValue move = movement.getSpeedValue(encumberance);
        move.setBase(speed);
    }


    void levelUpHP(int increaseHP)
    {
        hp.advanceLevel(increaseHP);
    }


    AdjustableValue getAdjustableValue(Values selection, StringTokenizer descriptor)
            throws IllegalArgumentException
    {
        // i.e. Ability.WIS or Defense.AC_Reg
        switch (selection) {
        case Defense:
            Defense.AC_Type acType = Defense.AC_Type.valueOf(descriptor.nextToken());
            return defenseStats.getArmorClass(acType);

        case Ability:
            Ability ability = Ability.valueOf(descriptor.nextToken());
            return abilities.get(ability);

        default:
            return null;
        }
    }


    Adjustment getAdjustment(Modifiers selection, StringTokenizer descriptor)
            throws IllegalArgumentException
    {
        String token = null;
        // i.e. Ability.WIS
        switch (selection) {
        case AbilityMod:
            token = descriptor.nextToken();
            LOG.debug("Looking up ability with token: " + token);
            Ability ability = Ability.valueOf(token);
            return abilities.get(ability).getModifier();

        default:
            break;
        }
        return null;
    }
}
